package ch.sv7.tool.dbmanager.db.xml;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.tools.ant.DirectoryScanner;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import ch.sv7.tool.dbmanager.StringUtil;
import ch.sv7.tool.dbmanager.db.AbstractDatabaseSchemaFactory;
import ch.sv7.tool.dbmanager.db.ColumnSchema;
import ch.sv7.tool.dbmanager.db.ColumnType;
import ch.sv7.tool.dbmanager.db.ConstraintSchema;
import ch.sv7.tool.dbmanager.db.ForeignKeySchema;
import ch.sv7.tool.dbmanager.db.IndexSchema;
import ch.sv7.tool.dbmanager.db.TableSchema;
import ch.sv7.tool.dbmanager.db.dialect.Dialect;
import ch.sv7.tool.dbmanager.db.dialect.DialectException;
import ch.sv7.tool.dbmanager.types.TypeInfo;
import ch.sv7.tool.log.Logger;

public class XmlTorqueDatabaseSchemaFactory extends AbstractDatabaseSchemaFactory {

	private static final Logger log = Logger.getLogger(XmlTorqueDatabaseSchemaFactory.class);
	
	private HashMap tables;
	private Properties typeMapping;
	private Dialect dialect;
	
	public XmlTorqueDatabaseSchemaFactory(File folder, String includes, String excludes, String schema, Properties typeMapping, boolean xmlValidate, Dialect dialect) {
		this.dialect = dialect;
		tables = new HashMap();
		this.typeMapping = typeMapping;
		if (this.typeMapping == null) {
			this.typeMapping = new Properties();
		}
		log.debug("XML validation of torque file is "+ (xmlValidate ? "on" : "off"));

		DirectoryScanner scanner = new DirectoryScanner();
		scanner.setBasedir(folder);
		if (includes != null){
			scanner.setIncludes(StringUtil.commaSeparatedToArray(includes));			
		} else {
			scanner.setIncludes(new String[]{"**/*.xml"});
		}
		if (excludes != null){
			scanner.setExcludes(StringUtil.commaSeparatedToArray(excludes));			
		}
		scanner.scan();
		
		String[] files = scanner.getIncludedFiles();
		for (int i=0; i<files.length; i++) {
			File file = new File(folder, files[i]);
			InputStream stream = null;
			try {
				log.info("Reading torque file:  " + file.getAbsolutePath());
				stream = new FileInputStream(file);
				processFile(stream, schema, xmlValidate, true, true);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}finally {
				if (stream != null) {
					try {
						stream.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

	private void processFile(InputStream stream, String schema, boolean validate, boolean renameIndexes, boolean renameForeignKeys) {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(validate);
		DocumentBuilder builder = null;
		try {
			builder = factory.newDocumentBuilder();
		} catch (ParserConfigurationException pce) {
			log.error(pce.getMessage());
			return;
		}
		try {
			/*
			 * Force disabling validation by loopbacking EntityResolver
			 */
			if (!validate) {
				builder.setEntityResolver(new EntityResolver() {
					public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
						return new InputSource(new ByteArrayInputStream(new byte[0]));
					}				
				});
			}
			Document document = builder.parse(stream);
			Element rootElement = document.getDocumentElement();
			NodeList nodes = rootElement.getElementsByTagName("table");
			for (int i = 0; i < nodes.getLength(); i++) {
				Element tableNode = (Element) nodes.item(i);
				String tableName = tableNode.getAttribute("name");
				TableSchema tableSchema = new TableSchema(tableName, schema);
				if (tables.containsKey(tableName)) {
					log.warn("WARNING: Table " + tableName + " is defined more than once, taking last definition!");
				}
				tables.put(tableName, tableSchema);
				NodeList optionNodes = tableNode.getElementsByTagName("option");
				for (int j=0; j<optionNodes.getLength(); j++) {
					Element optionNode = (Element)optionNodes.item(j);
					String key = optionNode.getAttribute("key");
					String value = optionNode.getAttribute("value");
					ConstraintSchema cs = new ConstraintSchema(tableSchema, key, value);
					tableSchema.getConstraints().add(cs);
				}
				
				NodeList fieldNodes = tableNode.getElementsByTagName("column");
				String pkFields = "";
				for (int j = 0; j < fieldNodes.getLength(); j++) {
					Element fieldNode = (Element) fieldNodes.item(j);
					String fieldName = fieldNode.getAttribute("name");
					String typeStr = fieldNode.getAttribute("type");
					String sizeStr = fieldNode.getAttribute("size");
					//String decimalSizeStr = fieldNode.getAttribute("decimalSize");
					String requiredStr = fieldNode.getAttribute("required");
					boolean primaryKey = "TRUE".equalsIgnoreCase(fieldNode.getAttribute("primaryKey"));
					String defaultValue = fieldNode.getAttribute("default");
					if (!fieldNode.hasAttribute("default")){
						defaultValue = null;
					}
					if ("NULL".equals(defaultValue)){
						defaultValue = null;
					}
					boolean autoIncrement = false;
					if (fieldNode.hasAttribute("autoIncrement")){
						autoIncrement = Boolean.valueOf(fieldNode.getAttribute("autoIncrement")).booleanValue();
					}
					ColumnSchema column = new ColumnSchema(
							tableSchema, 
							fieldName, 
							getDataType(typeStr, sizeStr, requiredStr, autoIncrement, defaultValue), 
							primaryKey);
					tableSchema.addColumnSchema(column);
					if (primaryKey) {
						if (pkFields.length() > 0) {
							pkFields += ", " + fieldName;
						} else {
							pkFields += fieldName;
						}
					}
				}
				
				if (pkFields.length() > 0) {
					IndexSchema pkIndex = new IndexSchema(tableSchema, tableName + "_pk", true, /* qualifier */ "", /*type*/"other", pkFields, true);
					tableSchema.getIndexes().add(pkIndex);
					
				}
				
				NodeList indexNodes = tableNode.getElementsByTagName("index");
				for (int j = 0; j < indexNodes.getLength(); j++) {
					Element indexNode = (Element) indexNodes.item(j);
					String name = indexNode.getAttribute("name");
					String columns = "";
					NodeList indexColumns = indexNode.getElementsByTagName("index-column");
					for (int k=0; k<indexColumns.getLength(); k++) {
						Element indexColumnNode = (Element) indexColumns.item(k);
						String columnName = formatCommaSeparated(indexColumnNode.getAttribute("name"));
						if (columns.length() != 0) {
							columns += ", ";
						}
						columns += columnName;
					}
					IndexSchema index = new IndexSchema(tableSchema, name, false, /*qualifier*/ "", /*type*/"other", columns, false);
					tableSchema.getIndexes().add(index);
				}	
				
				indexNodes = tableNode.getElementsByTagName("unique");
				for (int j = 0; j < indexNodes.getLength(); j++) {
					Element indexNode = (Element) indexNodes.item(j);
					String name = indexNode.getAttribute("name");
					String columns = "";
					NodeList indexColumns = indexNode.getElementsByTagName("unique-column");
					for (int k=0; k<indexColumns.getLength(); k++) {
						Element indexColumnNode = (Element) indexColumns.item(k);
						String columnName = formatCommaSeparated(indexColumnNode.getAttribute("name"));
						if (columns.length() != 0) {
							columns += ", ";
						}
						columns += columnName;
					}
					IndexSchema index = new IndexSchema(tableSchema, name, true,/*qualifier*/"", /*type*/"other", columns, false);
					tableSchema.getIndexes().add(index);
				}	
				
				NodeList fkNodes = tableNode.getElementsByTagName("foreign-key");
				for (int j = 0; j < fkNodes.getLength(); j++) {
					Element fkNode = (Element) fkNodes.item(j);
					String name = fkNode.getAttribute("name");
					String fTable = fkNode.getAttribute("foreignTable");

					String updateRule = fkNode.getAttribute("onUpdate");
					String deleteRule = fkNode.getAttribute("onDelete");
					//String deferrability = fkNode.getAttribute("deferrability");
					String columns = "";
					String fColumns = "";
					NodeList fkReferenceNodes = fkNode.getElementsByTagName("reference");
					for (int k=0; k<fkReferenceNodes.getLength(); k++) {
						Element fkReference = (Element) fkReferenceNodes.item(k);
						String local = fkReference.getAttribute("local");
						if (columns.length() != 0) {
							columns += ", ";
						}
						columns += local;
						
						String foreign = fkReference.getAttribute("foreign");
						if (fColumns.length() != 0) {
							fColumns += ", ";
						}
						fColumns += foreign;
					}

					ForeignKeySchema fk = new ForeignKeySchema(tableSchema, name, 
							columns, 
							fTable,
							fColumns,
							mapForeignKeyRule(updateRule),
							mapForeignKeyRule(deleteRule),
							"NotDeferrable");
					tableSchema.getForeignKeys().add(fk);
					
				}				

				if (renameIndexes){
					int idxCount = 1;
					int pkCount = 1;
					Iterator iter = tableSchema.getIndexes().iterator();
					while (iter.hasNext()) {
						IndexSchema indexSchema = (IndexSchema)iter.next();
						if (!indexSchema.isPrimaryKey()){
							indexSchema.setName(tableSchema.getName() + "_idx_" + idxCount++);
						} else {
							if (pkCount == 1) {
								indexSchema.setName(tableSchema.getName() + "_pk");
							} else {
								indexSchema.setName(tableSchema.getName() + "_pk_" + pkCount);
							}
							pkCount++;
						}
					}
				}
				
				if (renameForeignKeys){
					int count = 1;
					Iterator iter = tableSchema.getForeignKeys().iterator();
					while (iter.hasNext()){
						ForeignKeySchema fkSchema = (ForeignKeySchema)iter.next();
						fkSchema.setName(tableSchema.getName() + "_fk_" + count++);
					}
				}
			}

		} catch (FileNotFoundException e) {
			log.error(e.getMessage());
			return;
		} catch (SAXException e) {
			log.error(e.getMessage());
			return;
		} catch (IOException e) {
			log.error(e.getMessage());
			return;
		}		
	}
	
	private String formatCommaSeparated(String commaSeparated) {
		List cs = StringUtil.commaSeparatedToList(commaSeparated);
		return StringUtil.listToCommaSeparated(cs);
	}
	
	private String mapForeignKeyRule(String torqueRule){
		if ("cascade".equalsIgnoreCase(torqueRule)){
			return "Cascade";
		} else if ("setnull".equalsIgnoreCase(torqueRule)){
			return "SetNull";
		} else if ("restrict".equalsIgnoreCase(torqueRule)){
			return "Restrict";
		} else if ("none".equalsIgnoreCase(torqueRule)){
			return "Restrict";
		} else {
			return "Restrict";
		}
	}
	
	public TableSchema getTableSchema(String tableName) {
		return (TableSchema) tables.get(tableName);
	}

	public ColumnType getDataType(String typeStr, String sizeStr, String requiredStr, boolean autoIncrement, String defaultValue) {
		int type = 0;
		String typeName = null;
		if (typeMapping.containsKey(typeStr)){
			/*
			 * Custom mapped torque type using mappings parameter
			 */
			typeName = typeMapping.getProperty(typeStr);
			try {
				type = dialect.getTypesInfo().getTypeInfo(typeName).getJdbcType();
			} catch (DialectException e) {
				log.error(e.getMessage());
			}
		} else {
			/*
			 * Try default JDBC type
			 */
			type = ColumnType.stringToJdbcType(typeStr);
			if (type == 0 ){
				/*
				 * Default JDBC type not found, suppose it is a database specific type
				 */
				try {
					TypeInfo typeInfo = dialect.getTypesInfo().getTypeInfo(typeStr);
					if (typeInfo != null) {
						type = typeInfo.getJdbcType();
						typeName = typeStr;
					} else {
						throw new IllegalArgumentException("Unable to get information on type " + typeStr);
					}
				} catch (DialectException e) {
					log.error(e.getMessage());
				}
			} else {
				/*
				 * Default JDBC type found
				 */
				typeName = dialect.getDefaultTypeName(typeStr);
			}
		}

		long size = ColumnType.NO_SIZE_PROVIDED;
		long decimalSize = ColumnType.NO_SIZE_PROVIDED;
		int commaPos = sizeStr.indexOf(",");
		if (commaPos > 0){
			size = Long.parseLong(sizeStr.substring(0, commaPos).trim());
			decimalSize = Long.parseLong(sizeStr.substring(commaPos + 1).trim());
		} else if (sizeStr.length() > 0){
			if (sizeStr.endsWith("m")){
				size = Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)) * 1024L * 1024L;
			} else {
				size = Long.parseLong(sizeStr);
			}
		}
		boolean nullable = !("true".equalsIgnoreCase(requiredStr));
		ColumnType colType = new ColumnType(type, size, decimalSize, nullable, autoIncrement, typeName, defaultValue);
		return colType;
	}

	public String[] getTables() {
		return (String[])tables.keySet().toArray(new String[tables.size()]);
	}
	
}
